fix(icom): Restore durable Icom memory ownership and synced recall - #5328
Conversation
There was a problem hiding this comment.
Issue fit
There is no fixes/closes #NNNN in the commit (9a17060 Restore durable Icom memory ownership (Principle XI)), so I reviewed against the PR's own stated intent: make AetherSDR's shared client database the working memory store for every Icom, turn 1A 00 reads into an ingestion path rather than an ownership handover, and have repeated syncs update the same row instead of duplicating it. The first two are delivered cleanly and are, in my read, the right architectural call — persistsMemories was doing double duty as "who owns the store" and "can this radio be read", and splitting canRefreshMemories out of it is a genuine improvement over the previous profile-gated persistsMemories. The third claim does not hold in the one case that matters most; see Blocker 1.
Per GOVERNANCE.md this is close to an architectural change (it reverses which store owns Icom memories and bumps a persisted schema version), and it arrives with no linked issue or RFC. That is a maintainer call, not something I'd block on by itself, but it is worth noting that the capability semantics being changed here were themselves ratified through RFC #4603.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
IcomCivBackend.{h,cpp} |
persistsMemories→false, canRefreshMemories←profile, import identity + importSource/importKey/owner on the memory delta |
Yes | In scope |
RadioModel.cpp (applyMemoryChanges) |
Import routing: slot lookup, create-on-miss, persist | Yes | In scope |
LocalMemoryBank.{h,cpp} |
importedSlot() |
Yes | In scope |
LocalMemoryStore.{h,cpp} |
Persist 10 previously-unpersisted fields; kFormatVersion 1→2 |
Implied | In scope — see Nit 1 |
MemoryEntry.h, MemoryDelta.h |
importSource / importKey |
Yes | In scope |
MemoryDialog.cpp |
Comment; radio groups folded into the filter combo | Yes | In scope, but see Blocker 2 |
RadioCapabilities.h, both docs |
Comment/doc de-drift | Yes | In scope — one stale row left, see Nit 2 |
tests/* (3 files) |
New import-identity, round-trip, and gating assertions | Yes | In scope |
Nothing here is unrelated to the stated change. MemoryDelta::importSource/importKey is new cross-seam surface (backend → model), but it is internal C++, not a protocol verb or a settings key, and it is the mechanism the change requires. The persisted JSON schema bump is new durable surface — flagged below rather than waved through.
I did not find a false self-certification: no CHANGELOG.md entry (correct), no unrelated files, no formatting churn, no removed guards. Reading the - lines specifically: the only deleted logic is c.persistsMemories = memory != nullptr and c.canRefreshMemories = c.persistsMemories, both replaced deliberately; no comment naming a fixed symptom was dropped.
Blockers
1. The import identity is an IP address for every Icom, so a DHCP lease change permanently duplicates the whole channel set. (inline: IcomCivBackend.cpp:711)
The new comment says "Prefer the discovery identity so repeated syncs from the same radio update their rows; retain a deterministic endpoint fallback for a manually-entered radio that reports no serial." I could not reproduce a case where the first branch is ever taken. Icom has no discovery — ConnectionPanel.cpp:2739-2741 is the only producer of an Icom RadioInfo, and it always synthesizes info.serial = "icom:<ip>" (or "icom:<ip>:<port>" off the default base port), with its own comment saying "No discovery means no MAC and no reported serial, so the host is the only stable identity this radio has for us." That flows to req.serial at RadioModel.cpp:2436/:3456, so request.serial.trimmed() is never empty and the request.host fallback is dead code.
Net effect: m_memoryImportSource is always "icom:icom:<ip>". Failure scenario — an IC-9700 on DHCP, synced at 192.168.1.50, gets a new lease as 192.168.1.71, operator hits Sync again: importedSlot() matches nothing, applyMemoryChanges takes the targetIndex < 0 branch and calls memory create for every occupied channel, up to 297 fresh rows land beside the 297 already there. Same on the same radio reached via a hostname vs. an IP, or on the default base port vs. a custom one (different synthesized serial string). Nothing prunes the old rows: the d.removed path only forgets rows matching the current source. The ghosts are permanent, live in the durable settings-backed document that settings backup/export covers, and are visually indistinguishable in the dialog from the operator's own memories.
This falsifies the PR's and the design doc's central claim ("Repeated syncs update rows from the same radio/channel rather than duplicating them") — it is true only while the IP never changes, which is not a property a DHCP client has.
In fairness to the author: RadioModel::settingsScope() already keys per-radio state off this same IP-derived serial, so the PR is consistent with existing practice. The consequence differs, though. For settings, an address change means "per-radio settings reset" — annoying and self-limiting. Here it means the shared, backed-up memory database silently doubles and mixes ghost rows into the operator's list, with hand-deletion the only cleanup.
Fixes I'd consider, in preference order: (a) key importSource on something the radio actually reports — the CI-V address plus m_model->name is already decoded by adoptReportedCivAddress() and is address-independent; (b) keep the IP key but, at the end of a successful, complete refresh, reconcile: prune rows whose importSource names this same model but a stale endpoint; (c) at minimum, document the duplication and give the dialog a way to select all rows from one importSource. Option (a) also makes the "discovery identity" comment true.
2. On an IC-705, Sync now silently does nothing for most filter-combo selections. (inline: MemoryDialog.cpp:1484)
IcomCivBackend::refreshMemories() resolves groupName against the profile's group names and, when memory.requiresGroupSelection (the IC-705), bails with a bare return if it did not match (IcomCivBackend.cpp:1097-1099) — no signal, no toast, no memoryRefreshStarted. Before this PR the Icom combo contained only group names, so the sole way to hit that return was leaving it on "All Memories". This diff moves Icom onto the else branch, where the combo is relabelled "Profile:" and now carries global profiles + TX profiles + radio groups in one flat sorted list, and MemoryDialog.cpp:467 passes whatever is selected straight to refreshMemories().
Failure scenario: IC-705 operator with any global or TX profile defined, combo left on (or sorted to) a profile name, clicks Sync — the button appears to work and nothing whatsoever happens, with no way to tell that from a radio that answered with zero occupied channels. The blast radius is bigger than the old one-value case, and it is introduced by this hunk. Either gate/label the Sync affordance on the selection being a member of capabilities.memoryGroups, or have refreshMemories() report the refusal rather than returning silently.
Nits (non-blocking)
- The schema bump is a one-way door and is not called out.
kFormatVersion 1→2is backward-compatible on read (parse()only rejectsversion > kFormatVersion, so v1 documents load fine, and every new field reads through a defaulted accessor). It is not forward-compatible:LocalMemoryBank::load()'s row-version guard andparse()'s body check both make a v2 document read-only on any older build, so a downgrade after one sync leaves the operator's whole bank refusing edits with a "newer than this build" warning. That is the designed behaviour and the bump is defensible — dropping the new fields on an old build's write-back would be worse — but it deserves a sentence in the PR body. - Dead code the change leaves behind. With
persistsMemoriesnow false on every Icom and Flex the only backend setting it true,usesNativeMemorySchema(MemoryDialog.cpp:749-750and:1456-1457) is unconditionally false, so ~45 lines of native-column layout and the entireRadioCapabilities::memoryGroupColumnTitlefield (its only reader isMemoryDialog.cpp:757) are now unreachable. Likewise the whole!usesLocalMemoryBank()read-only-radio-store branch intryMemoryCommand(RadioModel.cpp:7797-7827): reaching it needspersistsMemories, which only Flex has, and Flex setscanWriteMemories/canApplyMemoriestrue so it falls straight toreturn nullopt. Not wrong, but it is a subsystem with no users, and the caps doc'scanApplyMemoriesrow still describes that path's behaviour as live ("applies recallable cached fields through the existing neutral slice setters") — worth either deleting or marking as reserved-for-future while the docs are being de-drifted anyway. - Lookup key is not normalized the way the stored key is.
applyMemoryChangesstoressanitize(*d.importSource)/sanitize(*d.importKey)(RadioModel.cpp:8078-8079) butimportedSlot()is called with the raw values (:8027).sanitizeTextstrips C0 and0x7f, which today's values never contain, so this is latent — but if it ever fires, the symptom is a fresh row on every sync forever, i.e. Blocker 1's failure mode with a harder-to-find cause. Cheapest fix is to sanitize once before both uses. - CodeGuard's two
CG-PATH-001hits are false positives —MemoryDialog.cpp:1194and:1420areQProgressDialog::setLabelTextformat strings ("Importing %1 of %2 memories…" / "Deleting %1 of %2 memories…"). No filesystem path is involved and neither line is in this diff. Dropping them.
What I tried to break
- "Repeated syncs update rows rather than duplicating them." Broke it — Blocker 1. Traced
importSourceback throughRadioConnectRequest.serialto its only producer to confirm thehostfallback is unreachable and the identity is always endpoint-derived. - "A radio's channel 1 cannot overwrite the operator's client slot 1." This one held, and I pushed on it.
allocateSlot()picks the lowest free index in the bank,publishLocalMemories()seedsm_memoriesfrom the bank on the connect edge viaonConnected → syncMemoryStoreForSession, and I confirmedonConnectedis wired toIRadioBackend::connected(RadioModel.cpp:1523) and not Flex-only — so the two maps are in sync before any create, and no aliasing is possible. Within one sync the ordering is safe too:record()writes the provenance pair before the next channel'simportedSlot()scan. - Radio-swap and disconnect lifecycle. Checked Flex→Icom and Icom→Flex.
onDisconnectedclearsm_memories, flushes the bank, and re-publishes (RadioModel.cpp:7053-7066), andsyncMemoryStoreForSessionclears before a radio-owned dump — so no leakage of Flex slots into the bank-published cache, in either direction. Them_sessionRadioOwnsMemorieslatch behaves correctly across a link blip on the new Icom path. - Failure paths. An unreadable/unwritable bank:
memory createrejects, the new code logs viaqCWarning(lcProtocol)and returns without touching the cache — correct.d.removedfor a channel never imported:targetIndex < 0, early return, no spuriousmemoryRemoved— correct. Non-occupied channels during an IC-705 group sweep behave. - Recall after import. Confirmed
MemoryEntry::recallabledefaults totrue, so manual/CSV rows still tune on an Icom now thatmemory applyroutes through the local bank intorecallCachedMemory()instead of the old read-only branch; and that the newly-persistednativeFilter/dataMode/rxToneValue/DTCS fields are exactly the onesrecallCachedMemory()feeds toapplyMemoryRecallDetails(), so a synced repeater channel survives a restart intact. This is the part of the PR I'd most want kept. - Filter/group namespace collision.
populateTablefilters onm.group, and the backend setsdelta.group = memoryGroupName(...), so the new combo entries do match imported rows — the added loop is necessary, not cosmetic, and it is in the liveelsebranch rather than the dead one. It did surface Blocker 2. - Attacked the tests.
radio_capability_gating_testadditions genuinely fail against the unfixed code (oldpersistsMemorieswastruefor all three profiles).local_memory_bank_test's reopen case is stronger than it looks — the second bank uses a different file path, so it proves the round trip goes through the shared settings document, not the legacy file. But nothing coversRadioModel::applyMemoryChanges's import routing, which is the actual headline behaviour: no test asserts that a radio channel lands in a fresh slot instead of slot 1, that a second sync reuses the row, or that an unoccupied channel forgets it.importedSlot()being correct is necessary, not sufficient.grepovertests/showsMemoryDeltaappears only inaetherd_residual_decode_test.cpp, so there is no existing harness to extend — which is probably why, but it is the test that would have caught Blocker 1. - What I could not check: CI is green on all four checks for
9a17060(build, check-windows, check-macos, Static checks), butctestinci.ymlis-R-filtered, so green does not mean the ~240-test suite passed. I have no build and no GUI here — every finding above is reasoned from the code at/tmp/aetherclaude/pr-5328, not reproduced at runtime. In particular I could not observe an actual IC-705/IC-9700 sync, so Blockers 1 and 2 are read off the call graph rather than watched happening.
Recommendation
Request changes, narrowly. The architecture is right and I'd like to see it land: separating "who owns the working store" from "can this radio be read as an import source" is a real improvement, the durable-recall field set is the part operators will feel, and the lifecycle handling around connect/disconnect/radio-swap survived everything I threw at it. But Blocker 1 makes the PR's own headline guarantee conditional on a stable IP, and the failure is silent, permanent, and lands in a backed-up document — that wants fixing before merge, ideally by keying the identity on the CI-V-reported model instead of the endpoint. Blocker 2 is a smaller, self-contained fix in the same area. Concrete next step: re-key m_memoryImportSource, add one regression test over applyMemoryChanges covering create-then-resync-then-remove, and either gate the Sync button on a real group selection or make refreshMemories say why it refused. Blocker 1's fix option (a) vs (b) is worth a maintainer opinion first, since (b) implies a pruning policy nobody has ruled on.
Thanks for the unusually careful doc and comment work here — the radio-capabilities-map.md rewrite of the persistsMemories row is clearer than what it replaced, and the reasoning comments in applyMemoryChanges made this reviewable.
🤖 aethersdr-agent · cost: $10.4712 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (2 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $11.5923 · model: claude-opus-5 |
9a17060 to
786c87d
Compare
786c87d to
776c7ca
Compare
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
#5327's regression (durable database hidden, Tune/Import broken, transient sync rows) is genuinely fixed, and the live automation-bridge validation on the IC-7300MK2 — 221 rows across restart, idempotent double-Sync, panadapter-spot recall — is exactly the right kind of evidence and covers what it claims. The provenance design (icom:<guid> + native <group>:<channel> key, all-zero GUID refused) is sound. The problems are at the edges the live run's own coverage-limits section honestly lists as untested — and several of them are the sharp edges.
Scope
All files map to the issue. One out-of-band item: none — the diff is clean. Preflight: no sockets, no fake peers; every new test is socket-free.
Blockers
1. Split/RPS recall is unevidenced, and recalling one pushes wrong state at a live radio (inline at the codec). The deleted rule was recallable = mode && !split && duplex != 3; the new rule keeps only the mode test, justified by a comment claiming "the local database already has a complete, neutral RX frequency" — but IcomMemoryChannel has no field for the second frequency block (it is parsed for nothing), and duplex == 3 falls through the backend's default: arm to offsetDir = "simplex". Tune on an RPS/split row therefore sets the slice to the first-block frequency and pushes DUP=simplex to the radio — clearing the stored split and, on transmit, keying on the wrong frequency. The PR body's own coverage-limits section says split/reverse-split recall was never live-tested, and the changed test asserts the flag, not that the recalled frequency is the RX side. Until the second block is decoded (or its semantics evidenced per the repo's docs/data/ precedent), duplex == 3 and split rows should stay display-only.
2. Re-Sync silently destroys operator edits on imported rows (inline at the stamp). delta.owner/delta.group/delta.name are unconditionally re-stamped on every pass, and applyMemoryChanges applies any present field to the matched row — so an operator who annotates Owner, fixes a name, or re-groups a synced channel loses those edits on the next Sync, persisted, with no prompt or diff. The body's "does not overwrite manual/CSV memories" is true and misses this class. Either skip fields the operator has touched (a dirty mask), stamp only on first insert, or warn.
3. The recallability repair heals partially, can be skipped entirely, and re-fires forever (inline). Three verified defects in one loop: (a) it keys on MemoryFields::isKnownMode, but the codec emits "CWL" and "WFM", neither in modes() — so CW-R/WFM rows poisoned by the first build stay display-only while their neighbours heal ("the fix mostly worked" bug reports); (b) it sits below if (!parsed.ok()) return;, so one non-fatal bad row anywhere in the document disables the whole repair; (c) it is un-versioned — it runs on every launch, so the moment DV/DD gain a neutral mapping (DSTR/FDV are already in modes()) every deliberately display-only row silently flips recallable, permanently, and no future codec decision can stick. The store's own kFormatVersion bump this PR makes is the migration hook the comment's "once" wants — gate the repair on storedRowVersion < 2, key it on the codec's rule, and move it into the store's schema layer.
4. The format-version bump is a one-way door for purely additive fields — and the repair walks users through it without any action of theirs. Every v2 field is read via value(...).toX(default), so a v1 build would parse a v2 document harmlessly; but flush() stamps kFormatVersion=2 on any save, load() refuses to write when stored > built, and the load-time repair itself triggers a flush. An operator who merely launches this build with synced rows and then rolls back finds an empty, read-only memory bank — indistinguishable from data loss, in the PR that exists because of an indistinguishable-from-data-loss bug. Don't bump the version for additive fields, or at minimum never auto-upgrade from load().
5. Tune half-applies then fails on a disconnected Icom. Imported rows always carry nativeFilter/offsetDir, so recall always reaches applyMemoryRecallDetails, which returns false without a session — after setMode/setFilterWidth/setFrequency have already run. The dialog is deliberately fully usable while disconnected, so this is a reachable ordinary path: half-applied slice plus a Tune error on a row presented as a normal database memory. Validate the backend requirement before mutating the slice.
6. Sync reports 100% success when zero rows were stored. The create-refusal and non-writable-bank paths are qCWarning-only, while the reply counter that feeds memoryRefreshFinished(true, n, n) increments before the delta is applied — an unreadable or full bank yields "297 of 297" and a success banner with nothing persisted. This is the exact silent-drop shape #5263 made loud on the command plane; thread a stored-count (or failure reason) back into the finished signal.
Also needing your ruling, not a blocker: persistsMemories = false for a radio that demonstrably persists and recalls its channels is in tension with Principle III's letter ("the deciding test is simply whether the radio can save and restore the value"). The mitigations are real — the client cannot write native memories, sync is one-directional, and both #5327 and the pre-#5283 design note prescribe this model — but III outranks the design doc, so the stale-recall trade (front-panel edit vs. un-resynced client row) should be ruled on explicitly rather than passed silently.
Nits (non-blocking, condensed)
usesNativeMemorySchemais now compile-time impossible (Icom hardcodespersistsMemories=false; only Flex sets true), leaving ~150 lines of dead two-schema table code, a write-onlymemoryGroupColumnTitlecapability, and — the real cost — the newly persistedchannel/nativeFilter/dataMode/rxToneValuefields rendered nowhere: a split-tone repeater's RX tone is stored, un-viewable, un-editable.- The Group-cell editor lost stored-group suggestions wherever
hasProfilesis true — including the never-connected default-Flex state, where the dropdown is now empty for an operator with 40 grouped rows (typo-fork risk); andhasProfilesis the wrong predicate for "who owns the memory vocabulary" (the deleted code keyed onpersistsMemories;MainWindow.cpp:7230shows the!connected ||form). The enable-check (case-insensitive, trimmed) and the backend accept-check (exact) also disagree, with the new gating test enshrining a string the backend rejects; and on IC-9700/MK2 a stored-group selection passes the GUI check and silently sweeps all groups. - Import identity (
importSource/importKey) is absent from the kv wire-codec and the CSV export, so a row round-tripped through either path loses its identity and the next Sync duplicates it — state the fields as backend→model-only inMemoryDelta.hor carry them explicitly. - Sync-time UI cost compounds: per-row
memoryChanged→ 50 ms full-table rebuilds × up to five by-valueRadioCapabilitiesconstructions per rebuild ≈ thousands of 700-field builds per 297-channel sweep; the already-wired refresh-finished signal should own the single repopulate. - The 47-byte IC-7300MK2 claim is live-radio fact with no
docs/data/evidence artifact (the repo's own IC-9700 precedent),layoutForstill sayssingleBytes=33while the comment says 33 never occurs, and the split-length fact belongs in the layout table (the inlinedialect !=test is exactly how the bug being fixed got introduced);docs/architecture/radio-capabilities-map.md'scanApplyMemoriesrow still says split/RPS are display-only, which this PR's own test now falsifies. - The identity-refusal guard — the advertised anti-aliasing protection — has no test reaching
refreshMemories, while the PR's ownradio_capability_gating_testpattern proves backend refusals are testable pre-transport; smaller:radioIdHexhand-rolls hex whereQByteArray::toHex()is the idiom two files away, imports insert twice via the command-string round-trip,flush()pretty-prints then re-parses the whole bank, and the repair logs "repaired N" even when the flush was refused.
What was verified vs read
- Verified by me in primary sources: the old vs new recallable rules and the absent second-frequency field; the
default:→simplex mapping; the owner/group/name stamps and their unconditional application;modes()vs the codec's"CWL"/"WFM"; the repair's position below theparsed.ok()early-return;persistsMemories=falsewith Flex as the onlytrueproducer and both renderers of the new fields behind the dead gate; main's editor-suggestion union vs the new spec. - Refuted along the way (claims the pass itself killed): the radioId ordering race, the 297×
load()/O(n²)-flush cost (loads latch, saves debounce), stale-Flex-rows leaking into imports, and my own initial "repair runs per command" reading (once per process). - Not run: no hardware, no bridge session of my own — the author's live IC-7300MK2 bridge run covers the happy paths well; every blocker above lives in the paths that run's coverage-limits section names as untested.
|
Thanks for this — it's a substantial piece of work, and the CI failure is a one-command fix, not anything wrong with the code. What failedOnly one job is red on Everything else in that job passed (engine/UI dependency direction, test registration, frozen PR gate, colour ratchet, a11y), and Why
Your new #include "core/backends/RadioCapabilities.h"That's a legitimate new gui→engine touchpoint, and it changes the Includers count for a row that already exists in the manifest. On current With your header it becomes 3, so the committed table is stale and Fixpython tools/gen_touchpoint_manifest.py
git add docs/architecture/aetherd-touchpoints.md
git commit --amend --no-edit # or a new signed commit, your preferenceThe diff should be one digit on line 170 (and the You can confirm before pushing with On the Copilot review commentsNeither reviewer blocker is connected to this CI failure — the manifest check is pure bookkeeping and doesn't read backend logic. Two notes on them anyway, since they'd otherwise look like they explain the red:
Once the manifest is regenerated, Static checks should go green. Nice work on the provenance keying and the version-2 format gate — the 🤖 aethersdr-agent · cost: $3.9476 · model: claude-opus-5 |
rfoust
left a comment
There was a problem hiding this comment.
Issue fit: the revised PR addresses #5327 by restoring the durable, editable client bank for Icom and making explicit native Sync an idempotent import. The prior split/RPS, annotation, normalization and load-repair blockers are corrected. The schema boundary is now limited to saves containing metadata that old writers cannot preserve safely; ordinary local banks remain version 1.
| File group | Claimed change | Scope verdict |
|---|---|---|
| Icom backend/session/protocol/codec | Stable authenticated radio identity, explicit Sync and safe recall classification | In scope |
| Memory models, bank and store | Durable imports, normalized identity, annotation preservation and complete recall fields | In scope |
| Capabilities, MemoryDialog and filter policy | Writable client bank with model-gated Sync and correct group vocabulary | In scope |
| Tests and registration | Decoder, persistence, capability and real signal-path regression coverage | In scope |
| Architecture docs and generated manifest | Document final behavior and compatibility; refresh dependency counts | In scope |
Everything in the final diff is explained by the issue and review corrections. No unrelated defaults, dependencies, protocol verbs or CHANGELOG entry were added. Current main's tuner assertions remain intact.
Blockers: none remaining in this review. Five addressed threads have concrete fix and regression evidence. Existing broader UI/CSV performance and native-model coverage notes remain non-blocking; the native metadata is not claimed to be fully editable or CSV-preserved.
Verified on source e87d783 (final head 8077d6b changes only the generated manifest), merged with current main a4227e6: full RADE-enabled ARM64 macOS app build; eight focused socket-free CTests; strict engine boundary, registration, CI gate, manifest and whitespace checks. Five independent mutations were rejected by the tests: split/RPS guard removal, unnormalized lookup, annotation overwrite, unconditional version 2, and load-time repair/write.
Runtime proof used only isolated offscreen DEMO-0001, with TX pinned off. The memory dialog displayed manual/imported/display-only rows and enabled local Add/Import/Remove. Normal GUI recall returned slice 0 at {frequency:14.25, mode:USB} and {frequency:7.05, mode:CW}; selecting the split fixture disabled Tune. The imported recall and disabled Tune survived closing/relaunching the process. Both review instances were closed. QRhi spectrum rendering is unavailable offscreen; no spectrum pixel or live Icom firmware convergence claim is made.
I tried to break slot collision handling, repeated/normalized Sync, empty-channel removal, cross-radio identity, locally edited annotations, restart before cache publication, safe recall classification and version-1 compatibility. The corrected code and tests held. No separate automated review skill was available; this includes direct issue-fit and lifecycle/failure-path review. Approval applies to 8077d6b; merge remains subject to current CI and repository protections.
Preserve current-main test removals while moving the memory assertions into focused registered targets. Keep native DATA state consistent after mode edits, publish final memory deltas before checked persistence/completion, report Sync refusals, reject disconnected native recall before slice mutation, and restore offline group suggestions. Verified: ARM64 app with RADE; eight focused socket-free CTests; five repeated import-test passes; deliberate pre-mutation guard, final-reply ordering, and offline-filter mutations fail. Isolated offscreen demo proof only; no live radio or TX.
rfoust
left a comment
There was a problem hiding this comment.
Issue fit
The durable Icom working-bank fix now includes the reviewed editing, failure-reporting and disconnected-recall edges. Reviewed 43f5493 against current main d58e2b8a94af82f273f60010df17b080e6b2d907. No remaining merge-blocking finding in this pass.
Scope
| File/group | Purpose | Verdict |
|---|---|---|
| LocalMemoryBank/Store, MemoryDelta/Entry, RadioModel | Durable imports, native fields, consistent edits, saved-result reporting | In scope |
| Icom backend/session/protocol/codec and capabilities | Stable identity, explicit safe native Sync and ordered completion | In scope |
| MemoryDialog/MemoryFilterPolicy | Writable local controls, offline groups and native-recall refusal | In scope |
| Focused memory/protocol tests and registration | Behavioral regressions and assertions moved out of main's retired target | In scope |
| Architecture documents/manifest and IRadioBackend signal comment | Ownership and completion-contract documentation | In scope |
Everything in the PR diff is explained by #5327. No unrelated dependencies, default changes, native-memory writes, TX or changelog entry.
Blocker disposition
- Split/RPS/DV/DD remain display-only; the unsafe broadened recall rule is absent.
- Repeat Sync preserves operator annotations.
- The speculative load-time recallability repair remains removed; only explicit Sync refreshes native safety metadata.
- Ordinary local banks remain schema 1; native/safety fields intentionally require schema 2, documented in the PR body.
- Native recall now refuses before slice changes while disconnected; failure propagates through the memory-command callback. The dialog states the connection requirement.
- Full/read-only/foreign-write failures cannot report successful Sync. The backend publishes its last occupied or empty-channel delta before completion, and the model checks the synchronous bank commit.
- Local mode edits no longer retain a contradictory native DATA flag.
- Current-main conflicts were resolved without resurrecting the broad test removed in #5452. PR-specific capability/filter assertions remain in registered socket-free targets. Offline stored-group suggestions were restored.
Evidence and limits
The ARM64 app build and eight focused CTests passed. New edit/save tests failed before the fixes. Deliberate mutations of final-reply ordering, the disconnected-recall guard and offline filter selection failed; the production fixes were restored, rebuilt and retested. Strict engine-boundary, test-registration, frozen CI-gate, manifest and whitespace checks passed.
The isolated offscreen demo read back DEMO-0001, model AetherSDR Demo, one slice/pan and transmitting=false; imported recall returned frequency=7.05, mode=CW. Before connecting, native Tune was disabled with its connection explanation and Local group remained selectable. Display-only Tune stayed disabled. Saved rows survived process restart; the review instance was stopped.
No live Icom firmware or TX validation was performed. Demo/control and socket-free model/codec evidence does not claim live firmware convergence. Existing native-metadata presentation/CSV-provenance limitations remain non-blocking follow-up scope, not newly delivered UI features.
All six technical blockers are addressed at signed head 43f5493 and revalidated in review #5124070129. The repair adds pre-mutation disconnected-recall refusal, checked Sync import/persistence results and final-delta ordering. Prior split safety, annotation preservation, no load-time repair and conditional schema compatibility remain tested. Operator authorized fixes and squash merge; CI remains a merge gate.
Closes #5327.
Summary
Keep AetherSDR's durable, writable memory database visible for every Icom model. Icom now declares
persistsMemories=false; model-gated, button-only Sync ingests native channels into the same local database used by manual and CSV memories. Flex retains its native writable store. Other client-bank backends retain their existing ownership.Sync and recall
CWL) can be recalled. Split, reverse-split, DV and DD remain display-only because the neutral path cannot represent their complete semantics.Persistence and compatibility
Native recall fields and provenance round-trip through the shared MemoryBank document. Loading an existing bank does not guess recallability, rewrite it or upgrade its schema. Experimental imports did not retain enough split/RPS metadata for a safe load-time repair: users of those builds should explicitly Sync again, not delete their database.
Ordinary local memories remain schema 1, including after edits. A save containing native recall fields or display-only safety metadata uses schema 2 because older writers discard those fields and default missing recallability to true. Future-schema and unreadable-document protections remain intact. After saving native imports, downgrade requires a compatible build or restoration of a pre-Sync backup; merely opening a legacy bank does not cross that boundary.
Current-main integration and validation
Validated repair revision: 43f5493, incorporating main
d58e2b8a94af82f273f60010df17b080e6b2d907.Moving-main revalidation: while CI ran, main advanced through the Icom AX.25 fix
4bf9c5a6, TCI IQ support5674c7ab, and Flex meter fixf39e7d27e1e60fe6c55b340177cf93788b98f0fe. Each combined cleanly without changing the repair head. The full ARM64/RADE desktop build and eight memory tests passed with4bf9c5a6; the engine and all eight tests were rebuilt and passed again with each later base. The final tested merge tree is265f46f8733cb4074b2bd0c0e5757efd302b8a5don mainf39e7d27. The generated manifest remains current. All five GitHub PR checks passed on the repair head (Linux, macOS, Windows, Static checks, Sanitizer option configures); their hosted merge base wasd58e2b8a, so the newer-base evidence is the explicit local revalidation above, not a claim that CI tested those later merges.radio_capability_gating_test(test: remove eight intermittently-failing tests. Principle VIII. #5452). Its PR-specific assertions now live in registeredmemory_filter_policy_testandmemory_import_test; the generated touchpoint manifest was regenerated on the resolved merge.icom_protocol_test,icom_memory_test,local_memory_store_test,local_memory_bank_test,memory_recall_policy_test,memory_csv_compat_test,memory_import_test,memory_filter_policy_test.memory_import_testuses direct production model/backend signals, an inert never-started IcomSession, and decoder input. No radio connection, bound socket, firmware peer, or TX is involved. Coverage includes collisions, source isolation, repeat Sync, annotations, deletion/persistence, native mode edits, full/future-schema/foreign-write refusals, final occupied/empty reply ordering, disconnected recall, and identity/group guards. The settings-isolation registration remains in place; the frozen PR CI allow-list is unchanged.Group:/Local groupand disabled native Tune with a connection explanation. After explicitDEMO-0001connection, readback showedAetherSDR Demo, one slice/pan andtransmitting=false; imported recall returned 7.05 MHz CW. Display-only Tune remained disabled. The three previously saved rows survived process restarts. The review instance was stopped; operator instances were untouched. This was control/model proof, not spectrum pixel validation.Safety and scope
No Icom native-memory writes, connect-time scans, TX validation, new dependencies, protocol verbs, defaults or feature-PR changelog entry. Client memory commands, persistence failures and existing offline behavior were repaired within #5327's scope. Main's unrelated changes were preserved through a non-rewriting merge.